{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 5.3 参数调优 - K折交叉验证 & GridSearch网格搜索"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**前情提要 - 5.2节的模型搭建代码**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "DecisionTreeClassifier(ccp_alpha=0.0, class_weight=None, criterion='gini',\n",
       "                       max_depth=3, max_features=None, max_leaf_nodes=None,\n",
       "                       min_impurity_decrease=0.0, min_impurity_split=None,\n",
       "                       min_samples_leaf=1, min_samples_split=2,\n",
       "                       min_weight_fraction_leaf=0.0, presort='deprecated',\n",
       "                       random_state=123, splitter='best')"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 1.读取数据与简单预处理\n",
    "import pandas as pd\n",
    "df = pd.read_excel('员工离职预测模型.xlsx')\n",
    "df = df.replace({'工资': {'低': 0, '中': 1, '高': 2}})\n",
    "\n",
    "# 2.提取特征变量和目标变量\n",
    "X = df.drop(columns='离职') \n",
    "y = df['离职']\n",
    "\n",
    "# 3.划分训练集和测试集\n",
    "from sklearn.model_selection import train_test_split\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=123)\n",
    "\n",
    "# 4.模型训练及搭建\n",
    "from sklearn.tree import DecisionTreeClassifier\n",
    "model = DecisionTreeClassifier(max_depth=3, random_state=123)\n",
    "model.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**5.3.1 K折交叉验证**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0.96666667, 0.96066667, 0.959     , 0.96233333, 0.91366667])"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.model_selection import cross_val_score\n",
    "acc = cross_val_score(model, X, y, cv=5)\n",
    "acc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.9524666666666667"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "acc.mean()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0.97146884, 0.9674637 , 0.96641351, 0.97047305, 0.95030156])"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.model_selection import cross_val_score\n",
    "acc = cross_val_score(model, X, y, scoring='roc_auc', cv=5)\n",
    "acc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.9652241309284616"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "acc.mean()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**5.3.2 GridSearch网格搜索**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**1.单参数的参数调优**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'max_depth': 7}"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.model_selection import GridSearchCV  # 网格搜索合适的超参数\n",
    "\n",
    "# 指定参数k的范围\n",
    "parameters = {'max_depth': [3, 5, 7, 9, 11]}\n",
    "# 构建决策树分类器\n",
    "model = DecisionTreeClassifier()  # 这里因为要进行参数调优，所以不需要传入固定的参数了\n",
    "\n",
    "# 网格搜索\n",
    "grid_search = GridSearchCV(model, parameters, scoring='roc_auc', cv=5)   # cv=5表示交叉验证5次，默认值为3；scoring='roc_auc'表示通过ROC曲线的AUC值来进行评分，默认通过准确度评分\n",
    "grid_search.fit(X_train, y_train)\n",
    "\n",
    "# 输出参数的最优值\n",
    "grid_search.best_params_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 通过取消如下代码的注释可以查看GridSearchCV函数的官方介绍\n",
    "# GridSearchCV?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**补充知识点：批量生成调参所需数据**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "parameters = {'max_depth': np.arange(1, 10, 2)}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**2.参数调优的效果检验**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "根据max_depth=7来重新搭建模型，并进行检测"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2.1 查看新模型准确度"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.982\n"
     ]
    }
   ],
   "source": [
    "# 根据max_depth=7来重新搭建模型\n",
    "model = DecisionTreeClassifier(max_depth=7)  # 这个max_depth参数是可以调节的，之后讲\n",
    "model.fit(X_train, y_train) \n",
    "\n",
    "# 查看整体预测准确度\n",
    "y_pred = model.predict(X_test)\n",
    "from sklearn.metrics import accuracy_score\n",
    "score = accuracy_score(y_pred, y_test)\n",
    "print(score)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "原来准确度评分score为0.957，现在为0.982，的确有所提升"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2.2 查看新模型的ROC曲线和AUC值"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 查看新的AUC值\n",
    "# 预测不违约&违约概率\n",
    "y_pred_proba = model.predict_proba(X_test)\n",
    "y_pred_proba[:,1]  # 如果想单纯的查看违约概率，即查看y_pred_proba的第二列\n",
    "\n",
    "# 绘制ROC曲线，计算AUC值\n",
    "from sklearn.metrics import roc_curve\n",
    "fpr, tpr, thres = roc_curve(y_test, y_pred_proba[:,1])\n",
    "\n",
    "# 绘制ROC曲线\n",
    "import matplotlib.pyplot as plt\n",
    "plt.plot(fpr, tpr)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9878435524589015\n"
     ]
    }
   ],
   "source": [
    "# 计算AUC值\n",
    "from sklearn.metrics import roc_auc_score\n",
    "score = roc_auc_score(y_test, y_pred_proba[:,1])\n",
    "print(score)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "总结：原来获得的AUC值为0.9736，现在获得的AUC值为0.9877，的确提高了模型的预测水平"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0.00059222, 0.52656655, 0.13229671, 0.1116004 , 0.07731135,\n",
       "       0.15163278])"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 查看此时的变量重要性\n",
    "model.feature_importances_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>特征名称</th>\n",
       "      <th>特征重要性</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>满意度</td>\n",
       "      <td>0.526567</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>工龄</td>\n",
       "      <td>0.151633</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>考核得分</td>\n",
       "      <td>0.132297</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>工程数量</td>\n",
       "      <td>0.111600</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>月工时</td>\n",
       "      <td>0.077311</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>工资</td>\n",
       "      <td>0.000592</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   特征名称     特征重要性\n",
       "1   满意度  0.526567\n",
       "5    工龄  0.151633\n",
       "2  考核得分  0.132297\n",
       "3  工程数量  0.111600\n",
       "4   月工时  0.077311\n",
       "0    工资  0.000592"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 一一对应\n",
    "features = X.columns\n",
    "importances = model.feature_importances_\n",
    "\n",
    "# 通过表格形式显示\n",
    "importances_df = pd.DataFrame()  # 创建空二维表格，为之后准备\n",
    "importances_df['特征名称'] = features\n",
    "importances_df['特征重要性'] = importances\n",
    "\n",
    "importances_df.sort_values('特征重要性', ascending=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**决策树模型还有些别的超参数，如下所示：**\n",
    "\n",
    "下面是分类决策树模型DecisionTreeClassifier()模型常用的一些超参数及它们的解释：\n",
    "1. criterion：特征选择标准，取值为\"entropy\"信息熵和\"gini\"基尼系数，默认选择\"gini\"。\n",
    "2. splitter：取值为\"best\"和\"random\"，\"best\"在特征的所有划分点中找出最优的划分点，适合样本量不大的情况，\"random\"随机地在部分划分点中找局部最优的划分点，适合样本量非常大的情况，默认选择\"best\"。\n",
    "3. max_depth：决策树最大深度，取值为int或None，一般数据或特征比较少的时候可以不设置，如果数据或特征比较多时，可以设置最大深度进行限制。默认取‘None’。\n",
    "4. min_samples_split：子节点往下划分所需的最小样本数，默认取2，如果子节点中的样本数小于该值则停止分裂。\n",
    "5. min_samples_leaf：叶子节点的最少样本数，默认取1，如果小于该数值，该叶子节点会和兄弟节点一起被剪枝（即剔除该叶子节点和其兄弟节点，并停止分裂）。\n",
    "6. min_weight_fraction_leaf：叶子节点最小的样本权重和，默认取0，即不考虑权重问题，如果小于该数值，该叶子节点会和兄弟节点一起被剪枝（即剔除该叶子节点和其兄弟节点，并停止分裂）。如果较多样本有缺失值或者样本的分布类别偏差很大，则需考虑样本权重问题。\n",
    "7. max_features：在划分节点时所考虑的特征值数量的最大值，默认取None，可以传入int型或float型数据。如果是float型数据，表示百分数。\n",
    "8. max_leaf_nodes：最大叶子节点数，默认取None，可以传入int型数据。\n",
    "9. class_weight：指定类别权重，默认取None，可以取\"balanced\"，代表样本量少的类别所对应的样本权重更高，也可以传入字典指定权重。该参数主要是为防止训练集某些类别的样本过多，导致训练的决策树过于偏向这些类别。除了此处指定class_weight，还可以使用过采样和欠采样的方法处理样本类别不平衡的问题，过采样和欠采样将在第十一章：数据预处理讲解。\n",
    "10. random_state：当数据量较大，或特征变量较多时，可能在某个节点划分时，会碰上两个特征变量的信息熵增益或者基尼系数减少量是一样的情况，那么此时决策树模型默认是随机从中选一个特征变量进行划分，这样可能会导致每次运行程序后生成的决策树不太一致。如果设定random_state参数（如设置为123）可以保证每次运行代码时，各个节点的分裂结果都是一致的，这在特征变量较多，树的深度较深的时候较为重要。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**3.多参数调优**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'criterion': 'entropy', 'max_depth': 11, 'min_samples_split': 13}"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.model_selection import GridSearchCV\n",
    "\n",
    "# 指定决策树分类器中各个参数的范围\n",
    "parameters = {'max_depth': [5, 7, 9, 11, 13], 'criterion':['gini', 'entropy'], 'min_samples_split':[5, 7, 9, 11, 13, 15]}\n",
    "# 构建决策树分类器\n",
    "model = DecisionTreeClassifier()  # 这里因为要进行参数调优，所以不需要传入固定的参数了\n",
    "\n",
    "# 网格搜索\n",
    "grid_search = GridSearchCV(model, parameters, scoring='roc_auc', cv=5)\n",
    "grid_search.fit(X_train, y_train)\n",
    "\n",
    "# 获得参数的最优值\n",
    "grid_search.best_params_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9823333333333333\n"
     ]
    }
   ],
   "source": [
    "# 根据多参数调优的结果来重新搭建模型\n",
    "model = DecisionTreeClassifier(criterion='entropy', max_depth=11, min_samples_split=13)\n",
    "model.fit(X_train, y_train) \n",
    "\n",
    "# 查看整体预测准确度\n",
    "y_pred = model.predict(X_test)\n",
    "from sklearn.metrics import accuracy_score\n",
    "score = accuracy_score(y_pred, y_test)\n",
    "print(score)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.9880075960970136\n"
     ]
    }
   ],
   "source": [
    "# 查看新的AUC值\n",
    "# 预测不违约&违约概率\n",
    "y_pred_proba = model.predict_proba(X_test)\n",
    "y_pred_proba[:,1]  # 如果想单纯的查看违约概率，即查看y_pred_proba的第二列\n",
    "\n",
    "score = roc_auc_score(y_test, y_pred_proba[:,1])\n",
    "print(score)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "总结：这里多参数调优后发现，模型效果的确有所优化"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**注意点1：多参数调优和分别单参数调优的区别**\n",
    "\n",
    "多参数调优和单参数分别调优是有区别的，比如有的读者为了省事，对上面的3个参数进行3次单独的单参数调优，然后将结果汇总，这样的做法其实是不严谨的。因为在进行单参数调优的时候，是默认其他参数取默认值的，那么该参数和其他参数都不取默认值的情况就没有考虑进来，也即忽略了多个参数对模型的组合影响。以上面的代码示例来说，使用多参数调优时，它是5*2*6=60种组合可能，而如果是进行3次单参数调优，则只是5+2+6=13种组合可能。\n",
    "因此，如果只需要调节一个参数，那么可以使用单参数调优，如果需要调节多个参数，则推荐使用多参数调优。\n",
    "\n",
    "**注意点2：参数取值是给定范围的边界**\n",
    "\n",
    "另外一点需要需要注意的是，如果使用GridSearchCV()方法所得到的参数取值是给定范围的边界，那么有可能存在范围以外的取值使得模型效果更好，因此需要我们额外增加范围，继续调参。举例来说，倘若上述代码中获得的最佳max_depth值为设定的最大值13，那么实际真正合适的max_depth可能更大，此时便需要将搜索网格重新调整，如将max_depth的搜索范围变成[9, 11, 13, 15, 17]，再重新参数调优。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
